﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace DDD {

    /// <summary>
    /// 複数のアニメーショントラックをまとめて再生速度・位置を制御するクラス
    /// </summary>
    /// <remarks>
    /// <dic>AnimationClip</dic>は複数の<see cref="AnimationTrack"/>を保持するコンテナクラスで、
    /// アニメーションを追加・削除する基本単位となります。
    /// <dic>AnimationClip</dic>は「歩く」「走る」などの一連のアニメーションを定義し、
    /// <see cref="AnimationTrack"/>は個々の関節１つ１つの回転動作を表します。
    /// ユーザーは<see cref="Object"/>の<see cref="Object.AddAnimation"/>メソッドを使って
    /// 任意のプロパティにアニメーションを追加可能です。
    /// また再生速度の変更やアニメーションの合成時のウェイトの制御も行います。
    /// <dic>AddEvent</dic>メソッドを使用して指定の時刻のクリップを再生したときに呼び出されるコールバック関数を設定可能です。
    /// (メモ)このターゲットって何で持ってるの？
    /// 必要ないと思うんだけど・・・
    /// </remarks>
    public class AnimationClip : Object {

        #region Field
        string name;
        float speed;
        float weight;
        int activeIntervalStart;
        int activeIntervalEnd;
        float worldReferenceTime;
        float localReferenceTime;
        List<Object> targets;           // これって何で保存してるんだっけ？？？。消せ
        List<AnimationTrack> tracks;
        List<AnimationEvent> events;

        #endregion

        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// このアニメーションの名前を指定してアニメーションクリップを作成します。
        /// クリップ名は任意の文字列でエンジン側では一切使用しません。
        /// </remarks>
        /// <param name="name">クリップの名前。</param>
        public AnimationClip (string name = "") {
            if (name == null) {
                throw new ArgumentNullException ("Name is null");
            }
            this.name = name;
            this.speed = 1;
            this.weight = 1;
            this.activeIntervalStart = 0;
            this.activeIntervalEnd = 0;
            this.worldReferenceTime = 0;
            this.localReferenceTime = 0;
            this.tracks = new List<AnimationTrack> ();
            this.events = new List<AnimationEvent> ();
            this.targets = new List<Object> ();
        }

        #endregion

        #region Property

        /// <summary>
        /// このクリップの名前
        /// </summary>
        /// <remarks>具体的な使い道は未定</remarks>
        public string Name {
            get { return name; }
        }

        /// <summary>
        /// 現在の再生速度
        /// </summary>
        public float Speed {
            get { return speed; }
            private set { this.speed = value; }
        }

        /// <summary>
        /// 現在のウェイト値
        /// </summary>
        /// <remarks></remarks>
        public float Weight {
            get { return weight; }
            private set { this.weight = value; }
        }

        /// <summary>
        /// ローカルリファレンス時間
        /// </summary>
        public float LocalReferenceTime {
            get { return localReferenceTime; }
            private set { this.localReferenceTime = value; }
        }

        /// <summary>
        /// ワールドリファレンス時間 
        /// </summary>
        public int WorldReferenceTime {
            get { return (int)worldReferenceTime; }
            private set { this.worldReferenceTime = value; }
        }

        /// <summary>
        /// このクリップがアクティブな区間の開始位置(ワールド時間)
        /// </summary>
        /// <remarks>
        /// アニメーションクリップはワールド時間で指定したアクティブインターバル[start,end)の区間だけ有効です。
        /// 区域外では設定によらず無効です。
        /// デフォルトの[0,0)は常に有効となります。
        /// <see cref="ActiveIntervalEnd"/>, <see cref="SetActiveInterval"/>
        /// </remarks>
        public int ActiveIntervalStart {
            get { return activeIntervalStart; }
            private set { this.activeIntervalStart = value; }
        }

        /// <summary>
        /// このクリップがアクティブな区間の終了位置(ワールド時間)
        /// </summary>
        /// <remarks>
        /// <see cref="ActiveIntervalEnd"/>, <see cref="SetActiveInterval"/>
        /// </remarks>
        public int ActiveIntervalEnd {
            get { return activeIntervalEnd; }
            private set { this.activeIntervalEnd = value; }
        }

        /// <summary>
        /// このクリップの標準的な長さ(ローカル時間)
        /// </summary>
        /// <remarks>
        /// このアニメーションクリップのローカル時間での長さです。
        /// 保有する全ての<see cref="AnimationTrack"/>のうち最長のものが、
        /// このクリップの<see cref="Duration"/>になります。
        /// </remarks>
        public int Duration {
            get {
                if (tracks.Count == 0) {
                    return 0;
                }
                return tracks.Max (track => track.KeyframeSequences.First ().Duration);
            }
        }


        /// <summary>
        /// このクリップが保有するトラックの本数
        /// </summary>
        /// <remarks>
        /// このアニメーションクリップを構成するアニメーショントラックの本数です。
        /// </remarks>
        public int TrackCount {
            get { return tracks.Count; }
        }

        /// <summary>
        /// このクリップにセットされたトラックの列挙子
        /// </summary>
        /// <remarks>
        /// このアニメーションクリップを構成するアニメーショントラックを列挙する列挙子です。
        /// </remarks>
        public IEnumerable<AnimationTrack> Tracks {
            get { return tracks; }
        }

        /// <summary>
        /// このクリップがバインドされたターゲットオブジェクトの数
        /// </summary>
        /// <remarks>
        /// このクリップが値を書き換えるオブジェクトの一覧です。
        /// これ何で保存してるんだっけ？？？
        /// 何かに使おうと思って定義したはずだが使用目的を忘れた。
        /// </remarks>
        public int TargetCount {
            get { return targets.Count; }
        }

        /// <summary>
        /// このクリップがセットされたターゲットを列挙するの列挙子
        /// </summary>
        /// <remarks>
        /// このクリップが値を書き換えるオブジェクトを列挙する列挙子です。
        /// </remarks>
        public IEnumerable<Object> Targets {
            get { return targets; }
        }

        /// <summary>
        /// 設定済みのイベントの数
        /// </summary>
        /// <remarks>
        /// </remarks>
        public int EventCount {
            get { return events.Count; }
        }

        /// <summary>
        /// このクリップにセットされたアニメーション・イベントを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// このアニメーションクリップにセットされたイベントを列挙する列挙子です。
        /// </remarks>
        public IEnumerable<AnimationEvent> Events {
            get { return events; }
        }

        /// <summary>
        /// このオブジェクトが参照する全DDDオブジェクトを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 参照にnullは含まれません。
        /// </remarks>
        public override IEnumerable<Object> References {
            get {
                var objs = base.References.ToList();
                objs.AddRange (tracks);
                objs.RemoveAll (x=>x==null);
                return objs;
            }
        }
        #endregion


        #region Method


        /// <summary>
        /// 指定のワールド時間でこのクリップが有効かどうかを判定します
        /// </summary>
        /// <param name="worldTime">ワールド時間</param>
        /// <returns></returns>
        public bool IsActive (int worldTime) {
            if (activeIntervalStart == activeIntervalEnd) {
                return true;
            }
            if (worldTime >= activeIntervalStart && worldTime < activeIntervalEnd) {
                return true;
            }

            return false;
        }


        /// <summary>
        /// 指定のインデックスのアニメーショントラックを取得します
        /// </summary>
        /// <param name="index">トラック番号</param>
        /// <returns>アニメーショントラック</returns>
        public AnimationTrack GetTrack (int index) {
            if (index < 0 || index >= TrackCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            return tracks[index];
        }

        /// <summary>
        /// 指定のインデックスのターゲットオブジェクトを取得します
        /// </summary>
        /// <param name="index">ターゲット番号</param>
        /// <returns>ターゲットオブジェクト</returns>
        public Object GetTarget (int index) {
            if (index < 0 || index >= TargetCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }

            return targets[index];
        }

        /// <summary>
        /// 指定のワールド時間に対応するローカル時間を取得します
        /// </summary>
        /// <remarks>
        /// 現在の設定で指定のワールド時間に対応するローカル時間を返します。
        /// (メモ)これGetPlaybackPosition()に変更しようか考慮中。
        /// </remarks>                              
        /// <param name="worldTime">ワールド時間(単位msec)</param>
        /// <returns>ローカル時間</returns>
        public float GetLocalTime (int worldTime) {
            return (worldTime - worldReferenceTime) * speed + localReferenceTime;
        }

        /// <summary>
        /// 指定のローカル時間に対応するワールド時間
        /// </summary>
        /// <remarks>
        /// 現在の設定で指定のローカル時間に対応するワールド時間を返します。
        /// <see cref="Speed"/>が0の場合は計算できず常に0が返ります。
        /// </remarks>
        /// <param name="localTime">ローカル時間</param>
        /// <returns>ワールド時間(単位msec)</returns>
        public float GetWorldTime (float localTime) {
            if (speed == 0) {
                return 0;
            }
            return (localTime - localReferenceTime) / speed + worldReferenceTime;
        }

        /// <summary>
        /// 指定のワールド時間に指定のローカル時間が一致するように再生位置を変更します。
        /// </summary>
        /// <remarks>
        /// 通常<paramref name="worldTime"/>には現在時刻を指定し、
        /// <paramref name="localTime"/>にはその時刻で再生したいクリップ位置をセットします。
        /// </remarks>
        /// <param name="worldTime">ワールド時間</param>
        /// <param name="localTime">ローカル時間</param>
        public void SetPlaybackPosition (int worldTime, float localTime) {
            this.worldReferenceTime = worldTime;
            this.localReferenceTime = localTime;
        }

        /// <summary>
        /// 再生速度を変更します
        /// </summary>
        /// <remarks>
        /// アニメーションクリップの再生速度を変更します。<see cref="Speed"/>=1.0が標準再生速度です。
        /// 2.0は倍の速度で再生します。-1.0は逆の順番で再生します。0.0は再生を止めます。
        /// ここで現在のワールド時間を指定するのは変更したあとでアニメーションが不連続にならないようにするためです。
        /// </remarks>
        /// <param name="speed">再生速度</param>
        /// <param name="worldTime">現在のワールド時間</param>
        public void SetSpeed (float speed, int worldTime) {
            this.localReferenceTime = GetLocalTime (worldTime);
            this.worldReferenceTime = worldTime;
            this.speed = speed;
        }

        /// <summary>
        /// ウェイトを設定します
        /// </summary>
        /// <param name="weight">ウェイト値</param>
        public void SetWeight (float weight) {
            this.weight = weight;
        }

        /// <summary>
        /// このクリップのアクティブな区間を設定します
        /// </summary>
        /// <remarks>
        /// <paramref name="start"/>==<paramref name="end"/>を指定すると全区間で常にアクティブになります。
        /// </remarks>
        /// <param name="start">アクティブ区間の開始時刻(ワールド時間)</param>
        /// <param name="end">アクティブ区間の終了時刻(ワールド時間)</param>
        public void SetActiveInterval (int start, int end) {
            if (end < start) {
                throw new ArgumentException ("Start, end is invalid");
            }

            this.activeIntervalStart = start;
            this.activeIntervalEnd = end;
        }

        /// <summary>
        /// アニメーショントラックを追加します
        /// </summary>
        /// <remarks>
        /// このアニメーションクリップに指定のトラックを追加します。
        /// </remarks>
        /// <param name="track">トラック</param>
        public void AddTrack (AnimationTrack track) {
            if (track == null) {
                throw new ArgumentNullException ("Track is null");
            }
            this.tracks.Add (track);
        }

        /// <summary>
        /// アニメーショントラック(複数)を追加します
        /// </summary>
        /// <remarks>
        /// このアニメーションクリップに指定のトラック(複数)を追加します。
        /// </remarks>
        /// <param name="tracks">追加したいアニメーショントラックの配列</param>
        public void AddTracks (AnimationTrack[] tracks) {
            if (tracks == null || tracks.Length == 0) {
                throw new ArgumentNullException ("Tracks is null");
            }
            this.tracks.AddRange (tracks);
        }

        /// <summary>
        /// アニメーショントラックを削除します
        /// </summary>
        /// <param name="track">トラック</param>
        public bool RemoveTrack (AnimationTrack track) {
            return tracks.Remove (track);
        }

        /// <summary>
        /// アニメーショントラック(複数)を削除します
        /// </summary>
        /// <param name="tracks">トラック</param>
        public void RemoveTracks (AnimationTrack[] tracks) {
            if (tracks == null) {
                return;
            }
            this.tracks.RemoveAll (x => tracks.Contains (x));
        }


        /// <summary>
        /// アニメーション・イベントを登録します
        /// </summary>
        /// <remarks>
        /// アニメーションクリップを再生中、指定の時刻にコールバック間数を呼び出すことができます。
        /// この仕組みをアニメーションイベントと呼びます。
        /// イベントは<paramref name="localTime"/>時刻(ローカル時間)と<paramref name="name"/>イベント名を指定して
        /// <cref name="AnimationEventHandler"/>型の関数を登録します。
        /// <paramref name="name"/>はこのイベントを識別する一意な名前です。
        /// 既に登録済みの名前は使用できません。
        /// </remarks>
        /// <param name="localTime">イベント発生時刻(ローカル時間)</param>
        /// <param name="name">イベント名</param>
        /// <param name="handler">呼び出されるコールバック関数</param>on
        /// <param name="arg">コールバック関数に渡される任意の引数１つ</param>
        public void AddEvent (float localTime, string name, AnimationEventHandler handler, object arg) {
            if (name == null) {
                throw new ArgumentNullException ("Name is null");
            }
            if (handler == null) {
                throw new ArgumentNullException ("Handler is null");
            }
            if (events.Any (x => x.Name == name)) {
                throw new ArgumentException ("Name is already used");
            }
            if (localTime < 0) {
                throw new ArgumentException ("localTime is invalid");
            }

            events.Add (new AnimationEvent (localTime, name, handler, arg));
        }

        /// <summary>
        /// アニメーション・イベントを削除します
        /// </summary>
        /// <param name="name">イベント名</param>
        public bool RemoveEvent (string name) {
            return events.RemoveAll (x => x.Name == name) > 0;
        }

        /// <summary>
        /// アニメーション・イベントを取得する
        /// </summary>
        /// <remarks>
        /// 設定済みのアニメーションイベントを取得します。
        /// </remarks>
        /// <param name="name">イベント名</param>
        /// <returns>一致するアニメーションイベントまたはnull</returns>
        public AnimationEvent GetEvent (string name) {
            return events.FirstOrDefault (x => x.Name == name);
        }


        /// <summary>
        /// 現在のオブジェクトの状態を表す文字列を返します。
        /// </summary>
        public override string ToString () {
            return string.Format ("AnimationClip: tracks={0}, targets={1}, events={2}", TrackCount, TargetCount, EventCount);
        }
        #endregion
    }
}
